Feature/s3 integration#41
Conversation
- Added S3Config with S3Client and S3Presigner - Implemented FileService for unique file uploads and presigned URL generation
- Integrated FileService (S3/MinIO) into visa creation and update flows. - Implemented logic for handling multipart file uploads and unique S3 key generation. - Added support for viewing and deleting documents via presigned URLs. - Refactored VisaDTO and UpdateVisaDTO to bridge backend storage with the UI. - Enhanced Thymeleaf templates with real-time file selection feedback (JS). - Fixed Controller rendering issues by ensuring the Model is populated during errors. - Verified workflow with integration tests for file uploads and validation.
📝 WalkthroughWalkthroughAdds MinIO-backed file storage: Docker Compose service and Maven S3 SDK; new S3 configuration beans; a FileService for uploads, presigned URLs, and deletions; VisaService/DTO/mapper/controller changes to store and surface S3 keys and download links; templates updated for multipart file UI; tests updated to mock FileService. Changes
Sequence DiagramsequenceDiagram
participant User as User
participant Controller as VisaViewController
participant FileSvc as FileService
participant MinIO as S3 (MinIO)
participant Service as VisaService
participant DB as Database
User->>Controller: POST /visas/apply (multipart passportFile)
Controller->>FileSvc: uploadFile(passportFile)
FileSvc->>MinIO: PUT object (bucket, uuid-key, fileData)
MinIO-->>FileSvc: 200 OK
FileSvc-->>Controller: s3Key
Controller->>Service: applyForVisa(dto, userId, s3Key)
Service->>DB: save Visa (with s3Key)
DB-->>Service: saved
Service-->>Controller: VisaDTO
Controller-->>User: confirmation
User->>Controller: GET /visas/{id}
Controller->>Service: findVisaDtoById(id)
Service->>FileSvc: getPresignedDownloadUrl(s3Key)
FileSvc->>MinIO: generate presigned URL (10m)
MinIO-->>FileSvc: presignedUrl
FileSvc-->>Service: presignedUrl
Service-->>Controller: VisaDTO with downloadUrls
Controller-->>User: display links
User->>Controller: POST /visas/{id}/documents/delete (s3Key)
Controller->>Service: removeVisaDocument(visaId, s3Key, userId)
Service->>DB: update Visa remove s3Key / set status
DB-->>Service: updated
Service->>FileSvc: deleteFile(s3Key)
FileSvc->>MinIO: DELETE object
MinIO-->>FileSvc: deleted
FileSvc-->>Service: success
Service-->>Controller: done
Controller-->>User: redirect to edit
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java (1)
318-336:⚠️ Potential issue | 🟡 MinorAssert
statusInformationon the service-error edit path.This test covers the
IllegalArgumentExceptionbranch but only checksmodel().hasErrors(). Add thestatusInformationassertion so the edit page feedback banner stays covered. Based on learnings,processUpdate()should addstatusInformationwhenvisaService.updateVisa()throwsIllegalArgumentExceptionsoedit-form.htmlrenders the feedback banner correctly.💚 Proposed test assertion
result.andExpect(status().isOk()) .andExpect(view().name("visa/edit-form")) - .andExpect(model().hasErrors()); + .andExpect(model().hasErrors()) + .andExpect(model().attributeExists("statusInformation"));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java` around lines 318 - 336, The test for the IllegalArgumentException branch (in VisaViewControllerTest) currently asserts view name and model errors but is missing a check that the controller sets the statusInformation model attribute when visaService.updateVisa(...) throws; update the multipart "/visas/{id}/edit" test to include an expectation like model().attributeExists("statusInformation") (or a specific value) after the existing andExpect calls so the edit-form feedback banner is covered; this relates to the processUpdate() path that should add the "statusInformation" attribute when updateVisa throws IllegalArgumentException.src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java (2)
189-217:⚠️ Potential issue | 🟠 MajorOrphaned S3 objects if
applyForVisa/updateVisafails after upload.The
fileService.uploadFile(...)call happens in the controller before this@Transactionalservice method runs (seeVisaViewController.submitApplicationLines 101–105 andprocessUpdateLines 175–179). IfvisaRepository.save(visa)here throws (constraint violation, connection lost, validation, etc.), the transaction rolls back but the S3 object remains — you accumulate orphaned uploads that are never referenced from the DB.Consider one of:
- Performing the upload inside a service method and, on DB-commit failure, compensating with
fileService.deleteFile(...)in atry/catcharound the save.- Using
TransactionSynchronizationManager.registerSynchronizationwithafterCompletion(status == ROLLED_BACK)to schedule the S3 delete.- A janitor job that reconciles unreferenced keys in the bucket against
visa_documents.Same concern applies to
updateVisa(Lines 220–259).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java` around lines 189 - 217, The controller currently uploads files (fileService.uploadFile) before calling the transactional service methods applyForVisa and updateVisa, causing orphaned S3 objects if visaRepository.save rolls back; modify the service methods (applyForVisa and updateVisa in VisaService) to either perform the upload inside the transactional boundary or register a rollback callback to delete the uploaded key(s): either move the upload into VisaService (so you can catch save failures and call fileService.deleteFile on failure) or use TransactionSynchronizationManager.registerSynchronization to run fileService.deleteFile when afterCompletion indicates ROLLED_BACK; ensure auditService.createAuditLog and visaRepository.save remain unchanged except for adding the compensating delete or synchronization logic to clean up s3Key(s).
220-259:⚠️ Potential issue | 🟡 MinorAlign blank-check for consistency and defensive programming.
Two observations:
applyForVisaguards withs3Key != null && !s3Key.isBlank(), butupdateVisaonly checksnewS3Key != null. Align the blank-check to prevent edge cases where an empty string could be added tos3Keys.- The
updateVisamethod appends new documents without removing prior uploads—the list and S3 storage grow with each edit. The UI supports multiple attachments (labeled "View Attachment 1", "View Attachment 2", etc.) and allows manual deletion, suggesting accumulation is intentional. Confirm whether this behavior is desired; if documents should be replaced on re-upload instead, add cleanup logic here.♻️ Align blank-check
- if (newS3Key != null) { + if (newS3Key != null && !newS3Key.isBlank()) { visa.getS3Keys().add(newS3Key); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java` around lines 220 - 259, The updateVisa method currently adds newS3Key when newS3Key != null, but applyForVisa guards with s3Key != null && !s3Key.isBlank(), so in updateVisa (method updateVisa, symbol newS3Key and visa.getS3Keys()) change the guard to check for non-blank strings (e.g., newS3Key != null && !newS3Key.isBlank()) to avoid adding empty keys; additionally decide whether accumulation is intended: if attachments should replace prior upload, implement cleanup in updateVisa by removing existing entries from visa.getS3Keys() and deleting those objects from S3 (use your S3 client) before adding the new key and saving; if accumulation is intended, leave the list-as-append behavior but add the blank check above to prevent empty entries.
♻️ Duplicate comments (1)
src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java (1)
85-120:⚠️ Potential issue | 🟠 MajorUpload-then-persist ordering can orphan S3 objects.
When the multipart upload succeeds (Line 102) but
visaService.applyForVisa(...)later fails (e.g., DB constraint, travel-date race), the uploaded object remains in S3 with no DB reference. Same pattern inprocessUpdate(Lines 175–179). The root-cause note and a suggested fix are in theVisaServicereview; from the controller's side, consider delegating the upload into the service so both operations live under a single transactional/compensation boundary, or at minimum callfileService.deleteFile(s3Key)in the catch blocks before returning the form.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java` around lines 85 - 120, The controller currently uploads to S3 before calling visaService.applyForVisa in submitApplication (and similarly in processUpdate), which can leave orphaned S3 objects if the DB operation fails; either move the upload logic into visaService.applyForVisa/processUpdate so the service can manage the upload within its transactional/compensation boundary, or if you keep upload in the controller, ensure any uploaded s3Key is cleaned up on failure by calling fileService.deleteFile(s3Key) (guarded by s3Key != null) in each catch block (IOException, IllegalArgumentException and any other failure paths) before returning the form; update submitApplication and processUpdate accordingly so S3 cleanup always happens on error.
🧹 Nitpick comments (15)
src/test/java/org/example/visacasemanagementsystem/visa/VisaMapperTest.java (1)
80-101: Consider adding a test for the newstatusInformationmapping behavior.The mapper now conditionally sets
statusInformationonly when non-null (VisaMapper.java:59-61). This test only exercises thenullbranch, leaving the non-null case uncovered. Worth adding an assertion that (a) a non-nullstatusInformationin the DTO is written through, and (b) anullvalue does not overwrite a pre-existing value onexistingVisa.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/org/example/visacasemanagementsystem/visa/VisaMapperTest.java` around lines 80 - 101, Add two assertions and a new test case exercising VisaMapper.updateEntityFromDTO's statusInformation branch: create an UpdateVisaDTO with a non-null statusInformation and assert existingVisa.getStatusInformation() is updated; also assert in the current test (or a new test) that when DTO.statusInformation() is null an existing non-null existingVisa.statusInformation remains unchanged. Reference the UpdateVisaDTO construction and the visaMapper.updateEntityFromDTO(...) call and assert against existingVisa.getStatusInformation() to cover both non-null and null branches.src/main/resources/application.properties (1)
21-22: Consider aligning multipart limits with S3/Tomcat expectations.10MB is reasonable for passport scans, but
VisaViewControlleraccepts the upload synchronously and streams through the service. If you ever accept larger documents, also tuneserver.tomcat.max-swallow-sizeandserver.tomcat.max-http-form-post-size, otherwise requests exceeding those (defaults 2MB/2MB respectively in some configurations) will fail before Spring enforces the multipart limit. Not a blocker at 10MB.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/application.properties` around lines 21 - 22, The multipart size properties spring.servlet.multipart.max-file-size and spring.servlet.multipart.max-request-size are set to 10MB but Tomcat can reject larger uploads first; update configuration to also set server.tomcat.max-swallow-size and server.tomcat.max-http-form-post-size to values equal to or larger than the multipart limits (or to -1 for unlimited) so uploads accepted by VisaViewController’s synchronous streaming won’t be dropped by Tomcat before Spring processes them; ensure the same limit is applied to both max-file-size and max-request-size and document the chosen values.compose.yaml (1)
11-24: Local-dev credentials and healthcheck.Two small improvements for the MinIO service:
- Parameterize credentials via
.envso they aren't duplicated betweencompose.yamlandapplication.properties, and so prod-ish environments don't silently reuseadmin/password.- Add a healthcheck so dependent services (app container, future init jobs) can wait for MinIO to be ready before
FileService's bucket-init runs.mc ready localor an HTTP probe on/minio/health/readyboth work.♻️ Suggested change
minio: image: 'minio/minio:latest' ports: - '9000:9000' - '9001:9001' environment: - - MINIO_ROOT_USER=admin - - MINIO_ROOT_PASSWORD=password + - MINIO_ROOT_USER=${MINIO_ROOT_USER:-admin} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-password} command: server /data --console-address ":9001" volumes: - minio_data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/ready"] + interval: 10s + timeout: 5s + retries: 5🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compose.yaml` around lines 11 - 24, The MinIO service currently hardcodes credentials and lacks a readiness probe; change the minio service to read MINIO_ROOT_USER and MINIO_ROOT_PASSWORD from environment variables (use ${MINIO_ROOT_USER} / ${MINIO_ROOT_PASSWORD} with sensible defaults via .env) instead of fixed "admin"/"password" so they can be shared with application.properties, and add a healthcheck block for the minio service (e.g., using "healthcheck" with an HTTP probe to /minio/health/ready or an sh -c 'mc alias set ... && mc admin info local' / 'mc ready local' command) so dependent services and FileService bucket-init wait until MinIO is ready; update .env with the variables and ensure application.properties references the same env vars.pom.xml (1)
156-160: Prefer AWS SDK BOM over pinning a single artifact version.Importing
software.amazon.awssdk:bomvia<dependencyManagement>keeps all AWS SDK modules (core, regions, auth, http-client, s3, etc.) aligned on one version and avoids subtle transitive-version mismatches as you add more SDK modules. Additionally, version2.42.30is now the latest stable release; update from2.42.20.♻️ Suggested structure
+ <dependencyManagement> + <dependencies> + <dependency> + <groupId>software.amazon.awssdk</groupId> + <artifactId>bom</artifactId> + <version>2.42.30</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> ... <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>s3</artifactId> - <version>2.42.20</version> </dependency>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pom.xml` around lines 156 - 160, Replace the hard-pinned software.amazon.awssdk:s3 dependency version with a BOM-based approach: add software.amazon.awssdk:bom at version 2.42.30 under dependencyManagement (import scope) and remove the <version> element from the existing <artifactId>s3</artifactId> dependency so the S3 module inherits the BOM-managed version; ensure any other AWS SDK modules follow the same pattern so all SDK modules (core, regions, auth, http-client, s3, etc.) remain aligned.src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java (1)
129-148: Stub and verify the S3 key flow in the upload tests.The tests at lines 129–148 and 224–245 attach
passportFilebut useany()for the S3 key parameter, allowing the controller to skipFileService.uploadFile()entirely (or passnull) and still pass. StubfileService.uploadFile()to return a specific key, verify the upload was called, and assert that exact key reachesVisaService.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java` around lines 129 - 148, The test currently uses any() for the S3 key allowing the controller to skip or ignore FileService.uploadFile(); update the test to stub fileService.uploadFile(...) to return a fixed S3 key (e.g. "s3://bucket/key.pdf"), assert fileService.uploadFile was called with the MockMultipartFile (passportFile), and change the verify of visaService.applyForVisa(...) to assert that the exact returned S3 key is passed into the CreateVisaDTO (or passed as the third parameter) instead of any(); locate MockMultipartFile creation, the mockMvc.perform multipart call, and the verify(visaService).applyForVisa(...) in this test and the similar block at lines ~224–245 to add the when(...).thenReturn(...) for fileService.uploadFile, a verify(fileService).uploadFile(...) call, and a stricter verification of the S3 key.src/test/java/org/example/visacasemanagementsystem/TestcontainersConfiguration.java (1)
15-18: Pin the PostgreSQL image instead oflatest.
postgres:latestis mutable; CI can silently move to a new major version and make Testcontainers runs non-reproducible. Pin the PostgreSQL major/minor version that matches production.♻️ Example pin, if PostgreSQL 16 matches the supported runtime
- return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest")) + return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine")) .withDatabaseName("testdb") .withUsername("test") .withPassword("test");Run this read-only check after updating to ensure mutable tags are gone:
#!/bin/bash # Description: Find mutable postgres image tags in test configuration. rg -n -C2 'postgres:latest|DockerImageName\.parse\("postgres:'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/org/example/visacasemanagementsystem/TestcontainersConfiguration.java` around lines 15 - 18, The Testcontainers configuration is using a mutable image tag ("postgres:latest") in the PostgreSQLContainer creation inside TestcontainersConfiguration; replace DockerImageName.parse("postgres:latest") with a pinned major.minor production-matching tag (e.g., "postgres:16.2" or the exact version you run in prod) or load that pinned tag from a test config/property, keeping the same PostgreSQLContainer construction and .withDatabaseName/.withUsername/.withPassword calls; after updating, run the provided grep/ripgrep check to ensure no remaining mutable postgres tags remain.src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java (1)
61-89:findVisaDtoByIdrebuilds the entireVisaDTOjust to inject presigned URLs.This manual re-enumeration of every field is fragile: any new field added to
VisaDTOmust be remembered here or the detail view silently loses it. It also duplicates responsibility withVisaMapper.toDTO(which setsdownloadUrlstoList.of()for the exact reason that this method will replace them).A cleaner approach is to push the "enrich with presigned URLs" step into a small helper (or a
VisaMapper.toDtoWithUrls(visa, fileService)overload), so only one code path constructsVisaDTOend-to-end.♻️ Proposed fix sketch
- public VisaDTO findVisaDtoById(Long id) { - Visa visa = visaRepository.findById(id) - .orElseThrow(() -> new EntityNotFoundException(NOT_FOUND_MESSAGE)); - - VisaDTO dto = visaMapper.toDTO(visa); - - List<String> presignedUrls = visa.getS3Keys().stream() - .map(fileService::getPresignedDownloadUrl) - .toList(); - - return new VisaDTO( - dto.id(), dto.visaType(), dto.visaStatus(), dto.nationality(), - dto.passportNumber(), dto.travelDate(), dto.applicantId(), - dto.applicantName(), dto.handlerId(), dto.handlerName(), - dto.createdAt(), dto.updatedAt(), dto.statusInformation(), - presignedUrls, dto.s3Keys() - ); - } + public VisaDTO findVisaDtoById(Long id) { + Visa visa = visaRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(NOT_FOUND_MESSAGE)); + return visaMapper.toDtoWithDownloadUrls(visa, fileService::getPresignedDownloadUrl); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java` around lines 61 - 89, The findVisaDtoById method manually rebuilds VisaDTO to set presignedUrls, which is fragile and duplicates VisaMapper.toDTO; instead add a single helper such as VisaMapper.toDtoWithUrls(Visa visa, FileService fileService) (or a small private method mapWithUrls) that calls toDTO(visa) and replaces/downloads the presigned URLs by mapping visa.getS3Keys() via fileService.getPresignedDownloadUrl, then return that DTO from findVisaDtoById; update findVisaDtoById to call the new mapper/helper and remove the manual reconstruction so future VisaDTO fields don’t need to be mirrored here.src/main/java/org/example/visacasemanagementsystem/config/S3Config.java (2)
18-28: Consider@ConfigurationPropertiesand secret-hardening for S3 credentials.Four individual
@Valueinjections duplicated across two beans work, but a single@ConfigurationProperties("minio")record/class would centralize the config and make it easier to validate (e.g.,@NotBlankonendpoint,accessKey,secretKey). Also flag: plainaccessKey/secretKeycoming from properties are fine for local MinIO, but for any non-dev deployment these should come from an env/secret store (SSM, Vault, K8s secrets) rather than be bundled intoapplication.properties.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/config/S3Config.java` around lines 18 - 28, Replace the scattered `@Value` injections in S3Config with a single `@ConfigurationProperties`(prefix="minio")-annotated class or record (e.g., MinioProperties) that declares endpoint, accessKey, secretKey, region with validation annotations like `@NotBlank`; inject that MinioProperties into S3Config and the other bean instead of duplicating fields, add `@Validated` on the configuration class to enforce constraints, and update documentation/config so accessKey/secretKey are sourced from environment/secret-management (SSM, Vault, or K8s secrets) for non-dev deployments rather than stored in application.properties.
38-42: Remove stray commented-out code.The
checksumValidationEnabled(false)line on Line 40 is commented out. If it's not needed, remove it to keep the configuration clean; if it was intended to be enabled to work around MinIO's handling of AWS SDK v2 default integrity checks, make it explicit rather than leaving it as a dead artifact.♻️ Suggested cleanup
.serviceConfiguration(S3Configuration.builder() .pathStyleAccessEnabled(true) -// .checksumValidationEnabled(false) .build())🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/config/S3Config.java` around lines 38 - 42, In S3Config update the S3Configuration builder call: either delete the commented-out ".checksumValidationEnabled(false)" line to remove dead code, or make the intent explicit by uncommenting and setting checksumValidationEnabled(false) on the S3Configuration builder (S3Configuration.builder() ... .checksumValidationEnabled(false) ... .build()) if you need to disable SDK integrity checks for MinIO; ensure the change is applied where S3Configuration is constructed inside the S3Config class.src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java (2)
189-203: Brittle message-string branching forIllegalArgumentException.
e.getMessage().contains("date")is a fragile way to distinguish travel-date errors from otherIllegalArgumentExceptions thrown by the service (e.g.,"Mismatched visa id.","This application can no longer be edited."). Any wording change inVisaServicesilently flips the error from a field error ontravelDateto a global error (or vice versa). Prefer a dedicated exception type (e.g.,InvalidTravelDateException) or return codes, and branch on the type rather than the message text.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java` around lines 189 - 203, The current catch in VisaViewController relies on inspecting IllegalArgumentException.getMessage() to detect travel-date errors (e.getMessage().contains("date")), which is brittle; update the service and controller to use a dedicated exception type instead: add an InvalidTravelDateException (or similar) thrown by VisaService when the travel date is invalid, then in the try/catch replace the message-inspection branch with a catch(InvalidTravelDateException e) that calls bindingResult.rejectValue("travelDate", "error.travelDate", e.getMessage()); keep a separate catch(IllegalArgumentException e) to handle other illegal-argument cases and call bindingResult.reject("globalError", e.getMessage()); ensure prepareApplyModel(principal.getUserId(), model), visaService.findVisaDtoById(id) and the model attributes remain as-is in both branches.
208-216: Document-delete endpoint has no user-visible error feedback on failure.
visaService.removeVisaDocument(...)can throwUnauthorizedExceptionor silently no-op if the key isn't attached. The controller simply redirects to/visas/{id}/editeither way; the user sees no confirmation on success nor an error on failure. Consider usingRedirectAttributes.addFlashAttribute(...)to surface both outcomes on the edit page. Also worth wrapping in a try/catch aroundEntityNotFoundExceptionso a bads3Keyreturns a friendly message instead of a 500 page via the global exception handler.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java` around lines 208 - 216, Update the deleteVisaDocument controller to catch and surface outcomes to the user: wrap the call to visaService.removeVisaDocument(id, s3Key, principal.getUserId()) in a try/catch that handles UnauthorizedException and EntityNotFoundException (and a generic Exception fallback), add a RedirectAttributes parameter to deleteVisaDocument, and use redirectAttributes.addFlashAttribute(...) to add a success message when removal succeeds or an error message when exceptions occur before returning "redirect:/visas/" + id + "/edit"; ensure UnauthorizedException produces a clear permission error and EntityNotFoundException produces a friendly "document not found" message rather than letting the global handler return 500.src/main/java/org/example/visacasemanagementsystem/file/FileService.java (2)
89-102:responseContentDispositionuses the raw S3 key as the filename.The key is
UUID + "_" + original, so users downloading always see the UUID prefix in their file name, and iforiginalever contained a"or newline (upstream sanitization would help, see theuploadFilecomment), it would break theContent-Dispositionheader. Consider storing the original filename as S3 object metadata at upload time and reading it back here (RFC 5987-encoded) so downloads present the original, safe filename.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/file/FileService.java` around lines 89 - 102, getPresignedDownloadUrl currently uses the raw S3 key (UUID_original) in the Content-Disposition; instead, store the original filename as object metadata at upload (e.g., metadata key "original-filename" in your uploadFile logic) and read that metadata here before building the presign. Call HeadObjectRequest/HeadObjectResponse (via your s3 client) to fetch metadata inside getPresignedDownloadUrl, fallback to the key if metadata is missing, RFC5987-encode the filename (UTF-8''percent-encoded) and ensure any quotes/newlines are sanitized, then set responseContentDisposition using filename*=UTF-8''encodedName so downloads show the safe original filename when you build the GetObjectRequest for s3Presigner.
41-53: Bucket verification swallows non-404 errors, then still attempts CORS configuration.If
headBucketfails with a non-404 S3Exception that's not rethrown into the outer catch (e.g., network error, auth error, region redirect), the outercatch (Exception e)logs and swallows, and execution falls through to the CORS block which will almost certainly also fail. Considerreturn-ing after a failed verify/create so CORS isn't attempted on an unverified bucket, and consider a health-indicator so operators can see the bucket wasn't provisioned rather than discovering it on the first user upload.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/file/FileService.java` around lines 41 - 53, The try/catch around s3Client.headBucket(...) swallows non-404 S3Exception into the outer catch and lets execution continue to the CORS configuration; update the FileService startup flow so that if headBucket fails for any non-404 S3Exception (or any exception when verifying/creating the bucket) you log the error and abort further bucket setup (e.g., return or throw to stop executing the CORS/configuration block) instead of falling through, and add or wire a health indicator/flag (e.g., bucketProvisioned boolean or health check method) that is set only after successful s3Client.createBucket(...) or headBucket verification so operators can detect an unprovisioned bucket.src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java (2)
59-61: Conditional set blocks clearingstatusInformationviaupdateEntityFromDTO.
dto.statusInformation() != nullmeans this mapper can never clear a previously set status message through the DTO; callers that want to clear it must do so out-of-band (asVisaService.updateVisacurrently does at Line 248 viavisa.setStatusInformation(null)). That's fine for the current flow, but be aware that any future caller expecting "null in DTO means clear" will silently keep the old value. If the intent is "only update when the DTO explicitly carries a value", that's okay — just flagging the semantic.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java` around lines 59 - 61, The current updateEntityFromDTO uses if (dto.statusInformation() != null) to skip setting statusInformation when the DTO value is null, which prevents callers from clearing an existing value; to fix, either (A) always call visa.setStatusInformation(dto.statusInformation()) in updateEntityFromDTO so a null in the DTO clears the field, or (B) add an explicit presence indicator to the DTO (e.g., hasStatusInformation() or Optional<String> statusInformation()) and update updateEntityFromDTO to set visa.setStatusInformation(...) only when that presence flag indicates the caller intended to update; update callers accordingly (e.g., VisaService.updateVisa) to use the chosen approach.
15-35:toDTO()returns aVisaDTOwith emptydownloadUrls, which is then reconstructed inVisaService.findVisaDtoById— fragile split responsibility.The mapper always sets
downloadUrlstoList.of()andfindVisaDtoByIdthen builds a brand-newVisaDTOcopying every field just to replacedownloadUrlswith presigned URLs (seeVisaService.findVisaDtoByIdLines 61–89). This has two consequences:
- Any other call site that uses
visaMapper.toDTO(visa)directly (e.g.,findAll,findVisasByApplicant,findVisaByType,findVisaByStatus,findVisaByDateCreated, etc.) silently returns visas with emptydownloadUrls, which is a trap for future callers/templates.- The reconstruction in
findVisaDtoByIdduplicates every field and will break silently if a new field is added toVisaDTOand forgotten there.Consider either (a) passing a
Function<String, String> s3KeyToUrl(or theFileService) into a dedicated mapper overload that populatesdownloadUrlsin one place, or (b) using a wither-style helper likedto.withDownloadUrls(urls)sofindVisaDtoByIddoesn't manually re-enumerate all fields.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java` around lines 15 - 35, The mapper VisaMapper.toDTO currently sets downloadUrls to an empty list causing callers to receive incomplete DTOs and forcing VisaService.findVisaDtoById to reconstruct the entire VisaDTO; change the design by either (A) adding an overload VisaMapper.toDTO(Visa visa, Function<String,String> s3KeyToUrl) or toDTO(Visa visa, FileService fileService) that maps visa.getS3Keys() -> presigned URLs and populates downloadUrls there, or (B) add a withDownloadUrls(List<String>) method on VisaDTO (a wither) and have VisaService.findVisaDtoById call visaMapper.toDTO(visa) and then dto.withDownloadUrls(urls) instead of reconstructing the DTO; update all call sites (findAll, findVisasByApplicant, findVisaByType, etc.) to use the appropriate overload or wither so downloadUrls is consistently populated in one place (refer to VisaMapper.toDTO, VisaService.findVisaDtoById, VisaDTO).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/java/org/example/visacasemanagementsystem/file/FileService.java`:
- Around line 56-70: The CORS rule is overly permissive: replace
allowedOrigins("*") and the wide allowedMethods list with a
configuration-driven, environment-safe policy—read allowed front-end origins
from configuration/env (e.g., FRONTEND_ORIGINS) and use those values in CORSRule
(instead of "*"), and restrict allowedMethods to the minimal set required for
browser interactions (e.g., "GET" and "HEAD", or include "POST" only if browser
uploads are used); update the block that builds CORSRule/CORSConfiguration
(references: CORSRule.builder(), allowedOrigins(), allowedMethods(),
PutBucketCorsRequest.builder(), bucketName, s3Client) to apply the tighter
policy and keep the permissive fallback only for local/dev (e.g., when
bucketName == "test-bucket" or an explicit env flag).
- Around line 73-87: In uploadFile(MultipartFile file) sanitize and validate
inputs before calling s3Client.putObject: treat file.getOriginalFilename() as
nullable and untrusted (handle null, strip path separators, control chars and
non-printable Unicode, and normalize/limit length) when building fileName;
validate file.getSize() against a configured maxFileSize (inject/config
property) and reject/throw if exceeded; validate file.getContentType() against
an allowlist (or detect/override using server-side detection) and do not blindly
echo an unverified content type into the S3 metadata; ensure you fail fast with
a clear exception for invalid files and use the sanitized fileName and validated
contentType when creating PutObjectRequest in uploadFile.
In
`@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java`:
- Line 214: The audit message in VisaService (where the string is built using
s3Key) misses a space and yields "Visa application submitted.Document
attached."; update the concatenation to include a space before "Document
attached." (e.g., change "Visa application submitted." + (s3Key != null ?
"Document attached." : "") to "Visa application submitted." + (s3Key != null ? "
Document attached." : "")) so s3Key-based messages read correctly.
- Around line 261-286: In removeVisaDocument, if visa.getS3Keys().remove(s3Key)
returns false you must surface that as an error instead of silently no-op: check
presence of the s3Key on the Visa (visa.getS3Keys()) and if absent throw an
EntityNotFoundException (e.g. new EntityNotFoundException("Document not attached
to visa: " + s3Key)); only call visaRepository.save(visa),
fileService.deleteFile(s3Key) and auditService.createAuditLog(...) when the key
was actually removed. Keep these changes inside the removeVisaDocument method so
ownership checks (isOwner/isAdmin), the status update, deletion and audit remain
unchanged when the key exists.
In `@src/main/resources/application.properties`:
- Around line 15-22: Remove the hard-coded MinIO credentials by replacing the
literal values for minio.accessKey and minio.secretKey with required Spring
placeholders that read from environment variables (e.g., ${MINIO_ACCESS_KEY} and
${MINIO_SECRET_KEY}) and fail fast if not provided (mirror the
spring.datasource.* pattern), ensure minio.endpoint/minio.bucketName remain
configurable but do not provide insecure defaults for secrets, and add
local/dev-only defaults in application-test.properties or a dev .env file
instead of committing them in this file.
In `@src/main/resources/templates/visa/apply-form.html`:
- Around line 215-223: The file-name display uses innerHTML with user-controlled
this.files[0].name, which is unsafe; in the event listener attached to
document.getElementById('passportFile') replace both assignments to
fileNameDisplay.innerHTML with fileNameDisplay.textContent (keep the same '📎
Selected: ' prefix for the first case and set an empty string for the else
branch) so the filename is rendered as plain text rather than parsed HTML.
In `@src/main/resources/templates/visa/details.html`:
- Around line 196-199: The "No documents attached" warning is shown to all
viewers; change the template logic to show the red/warning variant only for
regular users by checking currentUser.userAuthorization.name() == 'USER' and
render a neutral/info message for admins otherwise. Locate the existing
th:if="${`#lists.isEmpty`(visa.downloadUrls)}" block in the visa details template
and replace it with a role-gated conditional (use th:if to check empty
downloadUrls AND currentUser.userAuthorization.name() == 'USER' for the red
warning), plus an else/alternate block (e.g., th:if for admin or th:unless for
the USER check) that displays a neutral, non-alarming message when downloadUrls
is empty and the viewer is an admin.
- Around line 186-194: The links all show the hardcoded text "View Passport
Copy" because the template's th:each over visa.downloadUrls doesn't use any
per-item label; update the template loop (th:each="url,iter :
${visa.downloadUrls}") to use an index or pair with a parallel collection of
filenames returned from the backend (e.g., visa.s3Keys or visa.filenames) and
render a per-file label like the original filename or a contextual label using
the index (e.g., "View {filename}" or "View document #{iter.index+1}"); if
filenames are not yet returned, modify the backend to include a list of original
filenames (or a map of url->name) on the visa DTO so the template can display
distinct labels.
- Around line 365-376: The code creates a comment card using card.innerHTML and
directly interpolates comment.authorName and comment.text, which enables XSS;
instead, build the DOM using safe text nodes or set textContent for the author
and text (e.g., create elements for the .comment-meta and .comment-text, assign
their textContent from comment.authorName and comment.text, append them to card,
then container.prepend(card)) so no unescaped HTML from
comment.authorName/comment.text is inserted via innerHTML.
In `@src/main/resources/templates/visa/edit-form.html`:
- Around line 238-246: Change the file-name update to avoid HTML injection by
using textContent instead of innerHTML in the change handler attached to the
element with id 'passportFile' (the anonymous function passed to
document.getElementById('passportFile').addEventListener('change', ...)); set
fileNameDisplay.textContent = '📎 Selected: ' + this.files[0].name (and clear
with textContent = '') so the user-controlled this.files[0].name is not
interpreted as HTML.
- Around line 196-199: The anchor tag that renders presigned document links (the
<a> element with th:href="${url}" and the nested <span th:text="'View Attachment
' + ${iter.count}">) uses target="_blank" but lacks a rel attribute; update that
anchor to include rel="noopener noreferrer" to prevent the new tab from
accessing window.opener and mitigate security/privacy issues.
- Around line 229-234: The delete endpoint currently trusts the client-supplied
s3Key; update the service method that handles document deletion (the method
which calls fileService.deleteFile(s3Key)) to first verify ownership by checking
that the target Visa's collection contains the key (e.g., if
(!visa.getS3Keys().contains(s3Key)) throw new IllegalArgumentException("s3Key
does not belong to visa") or a domain-specific exception), and only then call
fileService.deleteFile(s3Key) and remove the key from visa; reference the Visa
entity's getS3Keys(), the service delete method that invokes
fileService.deleteFile, and ensure you persist the updated Visa after removal.
---
Outside diff comments:
In
`@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java`:
- Around line 189-217: The controller currently uploads files
(fileService.uploadFile) before calling the transactional service methods
applyForVisa and updateVisa, causing orphaned S3 objects if visaRepository.save
rolls back; modify the service methods (applyForVisa and updateVisa in
VisaService) to either perform the upload inside the transactional boundary or
register a rollback callback to delete the uploaded key(s): either move the
upload into VisaService (so you can catch save failures and call
fileService.deleteFile on failure) or use
TransactionSynchronizationManager.registerSynchronization to run
fileService.deleteFile when afterCompletion indicates ROLLED_BACK; ensure
auditService.createAuditLog and visaRepository.save remain unchanged except for
adding the compensating delete or synchronization logic to clean up s3Key(s).
- Around line 220-259: The updateVisa method currently adds newS3Key when
newS3Key != null, but applyForVisa guards with s3Key != null &&
!s3Key.isBlank(), so in updateVisa (method updateVisa, symbol newS3Key and
visa.getS3Keys()) change the guard to check for non-blank strings (e.g.,
newS3Key != null && !newS3Key.isBlank()) to avoid adding empty keys;
additionally decide whether accumulation is intended: if attachments should
replace prior upload, implement cleanup in updateVisa by removing existing
entries from visa.getS3Keys() and deleting those objects from S3 (use your S3
client) before adding the new key and saving; if accumulation is intended, leave
the list-as-append behavior but add the blank check above to prevent empty
entries.
In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`:
- Around line 318-336: The test for the IllegalArgumentException branch (in
VisaViewControllerTest) currently asserts view name and model errors but is
missing a check that the controller sets the statusInformation model attribute
when visaService.updateVisa(...) throws; update the multipart "/visas/{id}/edit"
test to include an expectation like model().attributeExists("statusInformation")
(or a specific value) after the existing andExpect calls so the edit-form
feedback banner is covered; this relates to the processUpdate() path that should
add the "statusInformation" attribute when updateVisa throws
IllegalArgumentException.
---
Duplicate comments:
In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`:
- Around line 85-120: The controller currently uploads to S3 before calling
visaService.applyForVisa in submitApplication (and similarly in processUpdate),
which can leave orphaned S3 objects if the DB operation fails; either move the
upload logic into visaService.applyForVisa/processUpdate so the service can
manage the upload within its transactional/compensation boundary, or if you keep
upload in the controller, ensure any uploaded s3Key is cleaned up on failure by
calling fileService.deleteFile(s3Key) (guarded by s3Key != null) in each catch
block (IOException, IllegalArgumentException and any other failure paths) before
returning the form; update submitApplication and processUpdate accordingly so S3
cleanup always happens on error.
---
Nitpick comments:
In `@compose.yaml`:
- Around line 11-24: The MinIO service currently hardcodes credentials and lacks
a readiness probe; change the minio service to read MINIO_ROOT_USER and
MINIO_ROOT_PASSWORD from environment variables (use ${MINIO_ROOT_USER} /
${MINIO_ROOT_PASSWORD} with sensible defaults via .env) instead of fixed
"admin"/"password" so they can be shared with application.properties, and add a
healthcheck block for the minio service (e.g., using "healthcheck" with an HTTP
probe to /minio/health/ready or an sh -c 'mc alias set ... && mc admin info
local' / 'mc ready local' command) so dependent services and FileService
bucket-init wait until MinIO is ready; update .env with the variables and ensure
application.properties references the same env vars.
In `@pom.xml`:
- Around line 156-160: Replace the hard-pinned software.amazon.awssdk:s3
dependency version with a BOM-based approach: add software.amazon.awssdk:bom at
version 2.42.30 under dependencyManagement (import scope) and remove the
<version> element from the existing <artifactId>s3</artifactId> dependency so
the S3 module inherits the BOM-managed version; ensure any other AWS SDK modules
follow the same pattern so all SDK modules (core, regions, auth, http-client,
s3, etc.) remain aligned.
In `@src/main/java/org/example/visacasemanagementsystem/config/S3Config.java`:
- Around line 18-28: Replace the scattered `@Value` injections in S3Config with a
single `@ConfigurationProperties`(prefix="minio")-annotated class or record (e.g.,
MinioProperties) that declares endpoint, accessKey, secretKey, region with
validation annotations like `@NotBlank`; inject that MinioProperties into S3Config
and the other bean instead of duplicating fields, add `@Validated` on the
configuration class to enforce constraints, and update documentation/config so
accessKey/secretKey are sourced from environment/secret-management (SSM, Vault,
or K8s secrets) for non-dev deployments rather than stored in
application.properties.
- Around line 38-42: In S3Config update the S3Configuration builder call: either
delete the commented-out ".checksumValidationEnabled(false)" line to remove dead
code, or make the intent explicit by uncommenting and setting
checksumValidationEnabled(false) on the S3Configuration builder
(S3Configuration.builder() ... .checksumValidationEnabled(false) ... .build())
if you need to disable SDK integrity checks for MinIO; ensure the change is
applied where S3Configuration is constructed inside the S3Config class.
In `@src/main/java/org/example/visacasemanagementsystem/file/FileService.java`:
- Around line 89-102: getPresignedDownloadUrl currently uses the raw S3 key
(UUID_original) in the Content-Disposition; instead, store the original filename
as object metadata at upload (e.g., metadata key "original-filename" in your
uploadFile logic) and read that metadata here before building the presign. Call
HeadObjectRequest/HeadObjectResponse (via your s3 client) to fetch metadata
inside getPresignedDownloadUrl, fallback to the key if metadata is missing,
RFC5987-encode the filename (UTF-8''percent-encoded) and ensure any
quotes/newlines are sanitized, then set responseContentDisposition using
filename*=UTF-8''encodedName so downloads show the safe original filename when
you build the GetObjectRequest for s3Presigner.
- Around line 41-53: The try/catch around s3Client.headBucket(...) swallows
non-404 S3Exception into the outer catch and lets execution continue to the CORS
configuration; update the FileService startup flow so that if headBucket fails
for any non-404 S3Exception (or any exception when verifying/creating the
bucket) you log the error and abort further bucket setup (e.g., return or throw
to stop executing the CORS/configuration block) instead of falling through, and
add or wire a health indicator/flag (e.g., bucketProvisioned boolean or health
check method) that is set only after successful s3Client.createBucket(...) or
headBucket verification so operators can detect an unprovisioned bucket.
In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`:
- Around line 189-203: The current catch in VisaViewController relies on
inspecting IllegalArgumentException.getMessage() to detect travel-date errors
(e.getMessage().contains("date")), which is brittle; update the service and
controller to use a dedicated exception type instead: add an
InvalidTravelDateException (or similar) thrown by VisaService when the travel
date is invalid, then in the try/catch replace the message-inspection branch
with a catch(InvalidTravelDateException e) that calls
bindingResult.rejectValue("travelDate", "error.travelDate", e.getMessage());
keep a separate catch(IllegalArgumentException e) to handle other
illegal-argument cases and call bindingResult.reject("globalError",
e.getMessage()); ensure prepareApplyModel(principal.getUserId(), model),
visaService.findVisaDtoById(id) and the model attributes remain as-is in both
branches.
- Around line 208-216: Update the deleteVisaDocument controller to catch and
surface outcomes to the user: wrap the call to
visaService.removeVisaDocument(id, s3Key, principal.getUserId()) in a try/catch
that handles UnauthorizedException and EntityNotFoundException (and a generic
Exception fallback), add a RedirectAttributes parameter to deleteVisaDocument,
and use redirectAttributes.addFlashAttribute(...) to add a success message when
removal succeeds or an error message when exceptions occur before returning
"redirect:/visas/" + id + "/edit"; ensure UnauthorizedException produces a clear
permission error and EntityNotFoundException produces a friendly "document not
found" message rather than letting the global handler return 500.
In
`@src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java`:
- Around line 59-61: The current updateEntityFromDTO uses if
(dto.statusInformation() != null) to skip setting statusInformation when the DTO
value is null, which prevents callers from clearing an existing value; to fix,
either (A) always call visa.setStatusInformation(dto.statusInformation()) in
updateEntityFromDTO so a null in the DTO clears the field, or (B) add an
explicit presence indicator to the DTO (e.g., hasStatusInformation() or
Optional<String> statusInformation()) and update updateEntityFromDTO to set
visa.setStatusInformation(...) only when that presence flag indicates the caller
intended to update; update callers accordingly (e.g., VisaService.updateVisa) to
use the chosen approach.
- Around line 15-35: The mapper VisaMapper.toDTO currently sets downloadUrls to
an empty list causing callers to receive incomplete DTOs and forcing
VisaService.findVisaDtoById to reconstruct the entire VisaDTO; change the design
by either (A) adding an overload VisaMapper.toDTO(Visa visa,
Function<String,String> s3KeyToUrl) or toDTO(Visa visa, FileService fileService)
that maps visa.getS3Keys() -> presigned URLs and populates downloadUrls there,
or (B) add a withDownloadUrls(List<String>) method on VisaDTO (a wither) and
have VisaService.findVisaDtoById call visaMapper.toDTO(visa) and then
dto.withDownloadUrls(urls) instead of reconstructing the DTO; update all call
sites (findAll, findVisasByApplicant, findVisaByType, etc.) to use the
appropriate overload or wither so downloadUrls is consistently populated in one
place (refer to VisaMapper.toDTO, VisaService.findVisaDtoById, VisaDTO).
In
`@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java`:
- Around line 61-89: The findVisaDtoById method manually rebuilds VisaDTO to set
presignedUrls, which is fragile and duplicates VisaMapper.toDTO; instead add a
single helper such as VisaMapper.toDtoWithUrls(Visa visa, FileService
fileService) (or a small private method mapWithUrls) that calls toDTO(visa) and
replaces/downloads the presigned URLs by mapping visa.getS3Keys() via
fileService.getPresignedDownloadUrl, then return that DTO from findVisaDtoById;
update findVisaDtoById to call the new mapper/helper and remove the manual
reconstruction so future VisaDTO fields don’t need to be mirrored here.
In `@src/main/resources/application.properties`:
- Around line 21-22: The multipart size properties
spring.servlet.multipart.max-file-size and
spring.servlet.multipart.max-request-size are set to 10MB but Tomcat can reject
larger uploads first; update configuration to also set
server.tomcat.max-swallow-size and server.tomcat.max-http-form-post-size to
values equal to or larger than the multipart limits (or to -1 for unlimited) so
uploads accepted by VisaViewController’s synchronous streaming won’t be dropped
by Tomcat before Spring processes them; ensure the same limit is applied to both
max-file-size and max-request-size and document the chosen values.
In
`@src/test/java/org/example/visacasemanagementsystem/TestcontainersConfiguration.java`:
- Around line 15-18: The Testcontainers configuration is using a mutable image
tag ("postgres:latest") in the PostgreSQLContainer creation inside
TestcontainersConfiguration; replace DockerImageName.parse("postgres:latest")
with a pinned major.minor production-matching tag (e.g., "postgres:16.2" or the
exact version you run in prod) or load that pinned tag from a test
config/property, keeping the same PostgreSQLContainer construction and
.withDatabaseName/.withUsername/.withPassword calls; after updating, run the
provided grep/ripgrep check to ensure no remaining mutable postgres tags remain.
In `@src/test/java/org/example/visacasemanagementsystem/visa/VisaMapperTest.java`:
- Around line 80-101: Add two assertions and a new test case exercising
VisaMapper.updateEntityFromDTO's statusInformation branch: create an
UpdateVisaDTO with a non-null statusInformation and assert
existingVisa.getStatusInformation() is updated; also assert in the current test
(or a new test) that when DTO.statusInformation() is null an existing non-null
existingVisa.statusInformation remains unchanged. Reference the UpdateVisaDTO
construction and the visaMapper.updateEntityFromDTO(...) call and assert against
existingVisa.getStatusInformation() to cover both non-null and null branches.
In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`:
- Around line 129-148: The test currently uses any() for the S3 key allowing the
controller to skip or ignore FileService.uploadFile(); update the test to stub
fileService.uploadFile(...) to return a fixed S3 key (e.g.
"s3://bucket/key.pdf"), assert fileService.uploadFile was called with the
MockMultipartFile (passportFile), and change the verify of
visaService.applyForVisa(...) to assert that the exact returned S3 key is passed
into the CreateVisaDTO (or passed as the third parameter) instead of any();
locate MockMultipartFile creation, the mockMvc.perform multipart call, and the
verify(visaService).applyForVisa(...) in this test and the similar block at
lines ~224–245 to add the when(...).thenReturn(...) for fileService.uploadFile,
a verify(fileService).uploadFile(...) call, and a stricter verification of the
S3 key.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ab82b416-ceb1-4ade-8ae0-f919a6ae77c8
📒 Files selected for processing (22)
compose.yamlpom.xmlsrc/main/java/org/example/visacasemanagementsystem/config/S3Config.javasrc/main/java/org/example/visacasemanagementsystem/file/FileService.javasrc/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.javasrc/main/java/org/example/visacasemanagementsystem/visa/dto/UpdateVisaDTO.javasrc/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.javasrc/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.javasrc/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.javasrc/main/resources/application.propertiessrc/main/resources/templates/visa/apply-form.htmlsrc/main/resources/templates/visa/details.htmlsrc/main/resources/templates/visa/edit-form.htmlsrc/test/java/org/example/visacasemanagementsystem/TestcontainersConfiguration.javasrc/test/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemVisaTests.javasrc/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.javasrc/test/java/org/example/visacasemanagementsystem/user/service/UserServiceIntegrationTest.javasrc/test/java/org/example/visacasemanagementsystem/visa/VisaMapperTest.javasrc/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.javasrc/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.javasrc/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.javasrc/test/resources/application-test.properties
| minio.endpoint=http://localhost:9000 | ||
| minio.accessKey=admin | ||
| minio.secretKey=password | ||
| minio.bucketName=visa-documents | ||
| minio.region=eu-north-1 | ||
|
|
||
| spring.servlet.multipart.max-file-size=10MB | ||
| spring.servlet.multipart.max-request-size=10MB |
There was a problem hiding this comment.
Don't commit hard-coded MinIO credentials as defaults.
minio.accessKey=admin and minio.secretKey=password are committed literal secrets. Even if these are only intended for local dev against the Compose MinIO, baking them into application.properties (which is also the profile loaded in prod unless overridden) means any deployment that forgets to override them runs with known credentials. Prefer required env vars with no insecure fallback for secrets, mirroring the pattern already used for spring.datasource.*.
🛡️ Suggested change
-minio.endpoint=http://localhost:9000
-minio.accessKey=admin
-minio.secretKey=password
-minio.bucketName=visa-documents
-minio.region=eu-north-1
+minio.endpoint=${MINIO_ENDPOINT:http://localhost:9000}
+minio.accessKey=${MINIO_ACCESS_KEY}
+minio.secretKey=${MINIO_SECRET_KEY}
+minio.bucketName=${MINIO_BUCKET:visa-documents}
+minio.region=${MINIO_REGION:eu-north-1}Then provide dev defaults via application-test.properties / local .env instead.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| minio.endpoint=http://localhost:9000 | |
| minio.accessKey=admin | |
| minio.secretKey=password | |
| minio.bucketName=visa-documents | |
| minio.region=eu-north-1 | |
| spring.servlet.multipart.max-file-size=10MB | |
| spring.servlet.multipart.max-request-size=10MB | |
| minio.endpoint=${MINIO_ENDPOINT:http://localhost:9000} | |
| minio.accessKey=${MINIO_ACCESS_KEY} | |
| minio.secretKey=${MINIO_SECRET_KEY} | |
| minio.bucketName=${MINIO_BUCKET:visa-documents} | |
| minio.region=${MINIO_REGION:eu-north-1} | |
| spring.servlet.multipart.max-file-size=10MB | |
| spring.servlet.multipart.max-request-size=10MB |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/resources/application.properties` around lines 15 - 22, Remove the
hard-coded MinIO credentials by replacing the literal values for minio.accessKey
and minio.secretKey with required Spring placeholders that read from environment
variables (e.g., ${MINIO_ACCESS_KEY} and ${MINIO_SECRET_KEY}) and fail fast if
not provided (mirror the spring.datasource.* pattern), ensure
minio.endpoint/minio.bucketName remain configurable but do not provide insecure
defaults for secrets, and add local/dev-only defaults in
application-test.properties or a dev .env file instead of committing them in
this file.
| <div th:if="${#lists.isEmpty(visa.downloadUrls)}" style="margin-top: 10px; color: #ff4d4d; font-size: 14px; display: flex; align-items: center; gap: 8px;"> | ||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> | ||
| No documents attached. This may delay processing. | ||
| </div> |
There was a problem hiding this comment.
"No documents attached" message is user-oriented but also shows to admins.
This block unconditionally warns "No documents attached. This may delay processing." — but admins viewing any undocumented case will also see this red warning, which reads as if directed at them. Consider gating the red/warning variant on currentUser.userAuthorization.name() == 'USER' and showing a neutral message for admin viewers.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/resources/templates/visa/details.html` around lines 196 - 199, The
"No documents attached" warning is shown to all viewers; change the template
logic to show the red/warning variant only for regular users by checking
currentUser.userAuthorization.name() == 'USER' and render a neutral/info message
for admins otherwise. Locate the existing
th:if="${`#lists.isEmpty`(visa.downloadUrls)}" block in the visa details template
and replace it with a role-gated conditional (use th:if to check empty
downloadUrls AND currentUser.userAuthorization.name() == 'USER' for the red
warning), plus an else/alternate block (e.g., th:if for admin or th:unless for
the USER check) that displays a neutral, non-alarming message when downloadUrls
is empty and the viewer is an admin.
| <div th:each="key, iter : ${visa.s3Keys}"> | ||
| <form th:id="'delete-doc-' + ${iter.index}" | ||
| th:action="@{/visas/{id}/documents/delete(id=${visa.id})}" | ||
| method="post" style="display: none;"> | ||
| <input type="hidden" name="s3Key" th:value="${key}"> | ||
| </form> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect document delete controller/service code for visa-id + s3Key ownership validation.
rg -n -C5 'documents/delete|removeVisaDocument|delete.*s3Key|getS3Keys\(\)\.contains|s3Keys'Repository: ithsjava25/visa-case-management-system
Length of output: 11243
Validate that the deleted s3Key belongs to the specified visa before removing.
The s3Key parameter is client-controlled and the delete endpoint does not verify that the key actually belongs to visa.s3Keys. Although ownership is checked (user must be the applicant), an attacker could attempt to delete documents from other visas by providing a valid visa id they own along with an s3Key from a different visa.
Add a check like if (!visa.getS3Keys().contains(s3Key)) throw... before calling fileService.deleteFile(s3Key) in the service method.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/resources/templates/visa/edit-form.html` around lines 229 - 234, The
delete endpoint currently trusts the client-supplied s3Key; update the service
method that handles document deletion (the method which calls
fileService.deleteFile(s3Key)) to first verify ownership by checking that the
target Visa's collection contains the key (e.g., if
(!visa.getS3Keys().contains(s3Key)) throw new IllegalArgumentException("s3Key
does not belong to visa") or a domain-specific exception), and only then call
fileService.deleteFile(s3Key) and remove the key from visa; reference the Visa
entity's getS3Keys(), the service delete method that invokes
fileService.deleteFile, and ensure you persist the updated Visa after removal.
There was a problem hiding this comment.
Suggested fix for this issue?
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
ast-grep --pattern 'public void removeVisaDocument($$$) {
$$$
}'
Length of output: 0
🏁 Script executed:
#!/bin/bash
rg -n "removeVisaDocument" --type java -A 30
Length of output: 7982
@eeebbaandersson Good news — the current code is partially protected already. On Line 274:
if (visa.getS3Keys().remove(s3Key)) {
// only runs if the key was actually in this visa's list
fileService.deleteFile(s3Key);
...
}List.remove() returns false when the key isn't present, so S3 deletion won't happen for a mismatched key. However, the code silently does nothing rather than rejecting the invalid input — which hides errors and is not explicit about intent.
Here is the suggested fix to add an explicit early validation in VisaService.java:
public void removeVisaDocument(Long visaId, String s3Key, Long userId) {
Visa visa = findVisaById(visaId);
// Check only the application owner or Admin/sysAdmin can remove files
User user = userRepository.findById(userId).orElseThrow();
boolean isOwner = visa.getApplicant().getId().equals(user.getId());
boolean isAdmin = user.getUserAuthorization() != UserAuthorization.USER;
if (!isOwner && !isAdmin) {
throw new UnauthorizedException("You cannot delete this document.");
}
+ // Verify the key actually belongs to this visa before touching S3
+ if (!visa.getS3Keys().contains(s3Key)) {
+ throw new IllegalArgumentException("Document does not belong to this visa application.");
+ }
if (visa.getS3Keys().remove(s3Key)) {
if (visa.getS3Keys().isEmpty()) {
visa.setVisaStatus(VisaStatus.INCOMPLETE);
} else {
visa.setVisaStatus(VisaStatus.SUBMITTED);
}
visaRepository.save(visa);
fileService.deleteFile(s3Key);
auditService.createAuditLog(userId, visaId, AuditEventType.UPDATED, "Removed document: " + s3Key);
}
}This gives you:
- Defense-in-depth — explicit rejection before any state is touched.
- Auditability — a clear exception instead of a silent no-op, making it easier to detect tampering attempts in logs.
- Correctness — the code's intent is explicit to future maintainers rather than relying on a side-effect of
List.remove().
🧠 Learnings used
Learnt from: eeebbaandersson
Repo: ithsjava25/visa-case-management-system PR: 27
File: src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java:38-41
Timestamp: 2026-04-14T13:13:34.322Z
Learning: In `visa-case-management-system`, the `processUpdate()` method in `VisaViewController` (`src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`, around lines 148–152) is missing `model.addAttribute("statusInformation", ...)` in the `IllegalArgumentException` catch block. When `visaService.updateVisa()` throws this exception, the edit form (`edit-form.html`) re-renders without `statusInformation`, causing the "Feedback from Handler" banner to display incorrectly. This is a UI/model bug independent of Spring Security and should be fixed by fetching the current visa's `statusInformation` (e.g., via `visaService.findVisaDtoById(id).statusInformation()`) and adding it to the model before returning the edit view.
Learnt from: eeebbaandersson
Repo: ithsjava25/visa-case-management-system PR: 27
File: src/main/resources/templates/visa/details.html:237-244
Timestamp: 2026-04-14T12:40:29.108Z
Learning: In `visa-case-management-system`, the comment form in `src/main/resources/templates/visa/details.html` (around lines 237-244) currently submits to `/api/comments` with form fields `visaId`, `authorId`, and `content` (wrong field name). The intended fix is to replace the HTML form with JavaScript/fetch to POST JSON to the existing REST endpoint `/api/comments`. The correct JSON body field for the comment text is `text` (matching `CreateCommentDTO`), not `content`. This is a known deferred issue to be addressed in a future PR.
Learnt from: eeebbaandersson
Repo: ithsjava25/visa-case-management-system PR: 16
File: src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java:42-43
Timestamp: 2026-04-09T11:13:00.080Z
Learning: In the `visa-case-management-system` repository, the `authorId` field in `CreateCommentDTO` is currently trusted directly in `CommentService.createComment()` (file: `src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java`, around line 42). This is a known security gap intentionally deferred until Spring Security is integrated. Once Spring Security is added, the author should be resolved from the authenticated principal (e.g., via `SecurityContextHolder`) rather than from `dto.authorId()`, and the `authorId` field should be removed from `CreateCommentDTO` or restricted to admin use only.
Learnt from: eeebbaandersson
Repo: ithsjava25/visa-case-management-system PR: 27
File: src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java:38-41
Timestamp: 2026-04-14T13:13:34.322Z
Learning: In `visa-case-management-system`, `VisaViewController` (`src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`) currently accepts `currentUserId` as a `RequestParam` in all handler methods (showDashboard, showApplyForm, submitApplication, showEditForm, processUpdate, approveVisa, requestMoreInformation, rejectVisa, assignCaseToHandler, viewDetails) instead of resolving the authenticated user from the Spring Security context. This is intentionally deferred until Spring Security is fully wired into the web layer. When implemented, `RequestParam Long currentUserId` should be removed from all these methods and replaced with resolution via `SecurityContextHolder.getContext().getAuthentication()` or `AuthenticationPrincipal`, followed by a `userService` lookup to obtain the `UserDTO`.
Learnt from: eeebbaandersson
Repo: ithsjava25/visa-case-management-system PR: 27
File: src/main/resources/templates/visa/dashboard.html:125-126
Timestamp: 2026-04-14T12:26:05.436Z
Learning: In the `visa-case-management-system` templates, ensure the “Sign Out” link/button uses the actual Spring Security logout mechanism once configured. Don’t leave it pointing to an unimplemented `/visas/logout` endpoint. When logout is ready, either (a) use Spring Security’s standard `/logout` endpoint via a POST form (including CSRF token handling), or (b) route to a dedicated controller method (e.g., a `VisaViewController` handler) that delegates to `SecurityContextLogoutHandler`.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
src/main/resources/templates/visa/details.html (1)
186-194:substring(visa.s3Keys[iter.index], 37)is brittle — tightly coupled to FileService's key format.The magic
37encodes "UUID (36 chars) +_(1 char)" fromFileService.uploadFile. If that prefix ever changes (e.g., switch to a prefix path likevisa/<id>/…, or a different id format), this throwsStringIndexOutOfBoundsExceptionat render time with no server-side signal. Safer options:
- Compute the display name in
VisaService.findVisaDtoByIdalongside the presigned URL and expose a parallelfileNameslist onVisaDTO.- Or do the stripping in a shared helper (e.g.,
FileService.stripKeyPrefix(key)) so the37lives in one place with the key-generation code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/templates/visa/details.html` around lines 186 - 194, The template uses a brittle hardcoded substring call (substring(visa.s3Keys[iter.index], 37)) tied to FileService.uploadFile key format; move the filename extraction out of the view by either (a) computing and attaching display names in VisaService.findVisaDtoById and adding a parallel fileNames list on VisaDTO to render instead of slicing s3Keys, or (b) add a single helper method in FileService (e.g., FileService.stripKeyPrefix(key)) that encapsulates the prefix logic and call that from the service layer to populate VisaDTO, then update the template to read the safe fileNames list (or sanitized keys) instead of using substring on visa.s3Keys.src/main/java/org/example/visacasemanagementsystem/file/FileService.java (1)
123-136: Presigned download filename leaks the UUID prefix.
responseContentDispositionusesfileName, which is the raw S3 key (<uuid>_<safeName>). Users who download the document will see e.g.a1b2c3…_passport.pdfas the save-as name. Since the key already has a fixedUUID + "_"prefix (37 chars), you can strip it here to present the original filename. If you ever want to show a true original name, consider persisting it alongside the key.✏️ Suggested change
- GetObjectRequest getObjectRequest = GetObjectRequest.builder() - .bucket(bucketName) - .key(fileName) - .responseContentDisposition("attachment; filename=\"" + fileName + "\"") - .build(); + String displayName = fileName.length() > 37 ? fileName.substring(37) : fileName; + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(fileName) + .responseContentDisposition("attachment; filename=\"" + displayName + "\"") + .build();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/file/FileService.java` around lines 123 - 136, The presigned download filename leaks the UUID prefix because getPresignedDownloadUrl uses the raw S3 key (fileName) in responseContentDisposition; update getPresignedDownloadUrl to derive a displayName by stripping the fixed UUID + "_" prefix (e.g. split on the first '_' or drop the first 37 chars) from fileName and use that displayName in responseContentDisposition so users see the original filename; keep bucketName and key as-is for the request, and consider persisting an originalName alongside the key (e.g., in your upload method) if you need a canonical source for the true original filename.src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java (2)
61-89:findVisaDtoByIddestructures the DTO just to swap one field — consider a helper.Rebuilding the 15-argument
VisaDTOrecord by copying every field from the mapped result is noisy and is a new place to forget a field the next timeVisaDTOgrows. Two cleaner options:
- Add a small copy-helper on
VisaDTO(e.g.withDownloadUrls(List<String>)) that returns a new record with only that field replaced.- Or have
VisaMapper.toDTOtake an optionalList<String> downloadUrls(defaultList.of()), and compute the URLs before calling the mapper.♻️ Example (withDownloadUrls on the record)
// In VisaDTO public VisaDTO withDownloadUrls(List<String> urls) { return new VisaDTO(id, visaType, visaStatus, nationality, passportNumber, travelDate, applicantId, applicantName, handlerId, handlerName, createdAt, updatedAt, statusInformation, urls, s3Keys); }- VisaDTO dto = visaMapper.toDTO(visa); - - List<String> presignedUrls = visa.getS3Keys().stream() - .map(fileService::getPresignedDownloadUrl) - .toList(); - - return new VisaDTO( dto.id(), /* …13 more fields… */ presignedUrls, dto.s3Keys()); + List<String> presignedUrls = visa.getS3Keys().stream() + .map(fileService::getPresignedDownloadUrl) + .toList(); + return visaMapper.toDTO(visa).withDownloadUrls(presignedUrls);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java` around lines 61 - 89, The method findVisaDtoById currently reconstructs a 15-arg VisaDTO to swap in presignedUrls; instead add a small copy-helper on the VisaDTO record (e.g. public VisaDTO withDownloadUrls(List<String> urls)) that returns a new VisaDTO with only the downloadUrls replaced, then change findVisaDtoById to call visaMapper.toDTO(visa) and then dto.withDownloadUrls(visa.getS3Keys().stream().map(fileService::getPresignedDownloadUrl).toList()) instead of rebuilding all fields; alternatively, if you prefer the mapper approach, adjust visaMapper.toDTO to accept an optional List<String> downloadUrls and pass the computed presignedUrls when calling it.
261-290:removeVisaDocument: theif (remove(s3Key))is effectively dead, andorElseThrow()is opaque.A couple of cleanups:
- Lines 274–276 already throw
IllegalArgumentExceptionwhen the key is not attached, so by the time execution reaches Line 278,remove(s3Key)always returnstrue. The outerif(and implicit silent no-op branch) can be dropped for clarity.- Line 266:
userRepository.findById(userId).orElseThrow()throws a bareNoSuchElementExceptionwith no message, which surfaces as a vague 500 ifuserIdis stale. Prefer anEntityNotFoundExceptionwith a message, matching the pattern used elsewhere in this class.- Consider also deleting from S3 after the in-memory remove and before
visaRepository.savevs. its current position — current order (save → delete S3) means iffileService.deleteFilefails, the key is already gone from the DB and the audit log still records success. For a dangling orphan that's arguably better than a dangling DB reference, but worth being deliberate about.♻️ Suggested simplification
- User user = userRepository.findById(userId).orElseThrow(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("User not found.")); boolean isOwner = visa.getApplicant().getId().equals(user.getId()); boolean isAdmin = user.getUserAuthorization() != UserAuthorization.USER; if (!isOwner && !isAdmin) { throw new UnauthorizedException("You cannot delete this document."); } if (!visa.getS3Keys().contains(s3Key)) { throw new IllegalArgumentException("Document does not belong to this visa application."); } - if (visa.getS3Keys().remove(s3Key)) { - if (visa.getS3Keys().isEmpty()) { - visa.setVisaStatus(VisaStatus.INCOMPLETE); - } else { - visa.setVisaStatus(VisaStatus.SUBMITTED); - } - visaRepository.save(visa); - - fileService.deleteFile(s3Key); - - auditService.createAuditLog(userId, visaId, AuditEventType.UPDATED, "Removed document: " + s3Key); - } + visa.getS3Keys().remove(s3Key); + visa.setVisaStatus(visa.getS3Keys().isEmpty() ? VisaStatus.INCOMPLETE : VisaStatus.SUBMITTED); + visaRepository.save(visa); + fileService.deleteFile(s3Key); + auditService.createAuditLog(userId, visaId, AuditEventType.UPDATED, "Removed document: " + s3Key);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java` around lines 261 - 290, In removeVisaDocument, simplify logic by removing the redundant if around visa.getS3Keys().remove(s3Key) (since you already validate membership), replace userRepository.findById(userId).orElseThrow() with a clear EntityNotFoundException including the userId (e.g., throw new EntityNotFoundException("User not found: " + userId)) and change the operation order so you remove the s3Key from visa.getS3Keys(), call fileService.deleteFile(s3Key) immediately after the in-memory removal (so deletion happens before persisting), then update visa status, call visaRepository.save(visa), and finally call auditService.createAuditLog(userId, visaId, AuditEventType.UPDATED, "Removed document: " + s3Key).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/java/org/example/visacasemanagementsystem/file/FileService.java`:
- Around line 68-88: The code in FileService.java builds a CORSRule from
corsAllowedOrigins which becomes an empty list when MINIO_CORS is blank, causing
S3 to reject the rule; update the logic (around corsAllowedOrigins/origins and
before CORSRule.builder() / s3Client.putBucketCors calls) to short-circuit when
origins.isEmpty() (skip applying CORS) or fallback to a safe default origin
(e.g., a single allowed origin like "http://localhost" or "*") and only call
CORSRule.builder().allowedOrigins(...) and s3Client.putBucketCors(...) when you
have at least one valid origin to send.
- Around line 54-88: The initializeBucket method in FileService currently
swallows bucket verification/creation failures which lets the subsequent CORS
setup (putBucketCors) run against a nonexistent bucket; update the outer catch
that logs "Failed to verify/create bucket" to stop further execution by either
rethrowing the caught exception (e.g., throw new RuntimeException("Failed to
verify/create bucket", e)) or returning from initializeBucket immediately after
logging, so that the CORS block (the code that builds CORSRule and calls
s3Client.putBucketCors) only runs when headBucket/createBucket succeeded;
reference s3Client.headBucket, s3Client.createBucket, the outer catch around
initializeBucket, and the putBucketCors call when making the change.
In `@src/main/resources/templates/visa/details.html`:
- Around line 378-379: Remove the stray leading "+" characters left from a diff
on the two DOM-manipulating expressions so they are normal statements: change
"+card.append(meta, textEl);" and "+container.prepend(card);" to
"card.append(meta, textEl);" and "container.prepend(card);" respectively (i.e.,
delete the unary-plus tokens affecting card.append and container.prepend).
---
Nitpick comments:
In `@src/main/java/org/example/visacasemanagementsystem/file/FileService.java`:
- Around line 123-136: The presigned download filename leaks the UUID prefix
because getPresignedDownloadUrl uses the raw S3 key (fileName) in
responseContentDisposition; update getPresignedDownloadUrl to derive a
displayName by stripping the fixed UUID + "_" prefix (e.g. split on the first
'_' or drop the first 37 chars) from fileName and use that displayName in
responseContentDisposition so users see the original filename; keep bucketName
and key as-is for the request, and consider persisting an originalName alongside
the key (e.g., in your upload method) if you need a canonical source for the
true original filename.
In
`@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java`:
- Around line 61-89: The method findVisaDtoById currently reconstructs a 15-arg
VisaDTO to swap in presignedUrls; instead add a small copy-helper on the VisaDTO
record (e.g. public VisaDTO withDownloadUrls(List<String> urls)) that returns a
new VisaDTO with only the downloadUrls replaced, then change findVisaDtoById to
call visaMapper.toDTO(visa) and then
dto.withDownloadUrls(visa.getS3Keys().stream().map(fileService::getPresignedDownloadUrl).toList())
instead of rebuilding all fields; alternatively, if you prefer the mapper
approach, adjust visaMapper.toDTO to accept an optional List<String>
downloadUrls and pass the computed presignedUrls when calling it.
- Around line 261-290: In removeVisaDocument, simplify logic by removing the
redundant if around visa.getS3Keys().remove(s3Key) (since you already validate
membership), replace userRepository.findById(userId).orElseThrow() with a clear
EntityNotFoundException including the userId (e.g., throw new
EntityNotFoundException("User not found: " + userId)) and change the operation
order so you remove the s3Key from visa.getS3Keys(), call
fileService.deleteFile(s3Key) immediately after the in-memory removal (so
deletion happens before persisting), then update visa status, call
visaRepository.save(visa), and finally call auditService.createAuditLog(userId,
visaId, AuditEventType.UPDATED, "Removed document: " + s3Key).
In `@src/main/resources/templates/visa/details.html`:
- Around line 186-194: The template uses a brittle hardcoded substring call
(substring(visa.s3Keys[iter.index], 37)) tied to FileService.uploadFile key
format; move the filename extraction out of the view by either (a) computing and
attaching display names in VisaService.findVisaDtoById and adding a parallel
fileNames list on VisaDTO to render instead of slicing s3Keys, or (b) add a
single helper method in FileService (e.g., FileService.stripKeyPrefix(key)) that
encapsulates the prefix logic and call that from the service layer to populate
VisaDTO, then update the template to read the safe fileNames list (or sanitized
keys) instead of using substring on visa.s3Keys.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ba544f7d-2b6a-437c-b4fb-15f916a767a8
📒 Files selected for processing (7)
src/main/java/org/example/visacasemanagementsystem/file/FileService.javasrc/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.javasrc/main/resources/application.propertiessrc/main/resources/templates/visa/apply-form.htmlsrc/main/resources/templates/visa/details.htmlsrc/main/resources/templates/visa/edit-form.htmlsrc/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/resources/templates/visa/edit-form.html
- src/main/resources/templates/visa/apply-form.html
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/main/java/org/example/visacasemanagementsystem/file/FileService.java (3)
129-142: Presigned download filename includes the UUID prefix.
responseContentDispositionis set toattachment; filename="<UUID>_<sanitized-original>", so end users downloading a passport document will see the internal UUID in the saved filename. Consider storing the original (sanitized) filename alongside the S3 key (e.g., as object metadata or in the DB) and passing only the user-friendly portion toresponseContentDispositionwhile keeping the UUID in the S3 key for uniqueness.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/file/FileService.java` around lines 129 - 142, getPresignedDownloadUrl currently sets responseContentDisposition to the full S3 key (which includes the internal UUID) so users download filenames like "<UUID>_original"; instead, change getPresignedDownloadUrl to look up the stored original/sanitized filename (from object metadata or your DB) and pass only that user-friendly name to GetObjectRequest.responseContentDisposition while keeping the UUID-prefixed S3 key for bucketName/key; update the method to fetch the original filename (via your metadata lookup), set responseContentDisposition to "attachment; filename=\"<originalFilename>\"" and continue using s3Presigner.presignGetObject with the existing GetObjectPresignRequest.
97-108: Prefer a validation-specific exception overIOExceptionfor size/type rejections.Throwing
IOExceptionfor user-input validation failures (size exceeded, disallowed content type) conflates client errors with genuine I/O failures. Upstream (VisaViewController) will likely translate this into a 500, whereas these are really400 Bad Requestconditions. ConsiderIllegalArgumentExceptionor a dedicatedInvalidUploadExceptionso the web layer can map it to a proper status and user-friendly message.♻️ Proposed refactor
- public String uploadFile(MultipartFile file) throws IOException { - // Validate file size - if (file.getSize() > MAX_FILE_SIZE_BYTES) { - throw new IOException("File exceeds maximum allowed size of 10 MB"); - } - - // Validate content type - String contentType = file.getContentType(); - if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) { - throw new IOException("Unsupported document type: " + contentType - + ". Allowed: PDF, JPEG, PNG"); - } + public String uploadFile(MultipartFile file) throws IOException { + // Validate file size + if (file.getSize() > MAX_FILE_SIZE_BYTES) { + throw new IllegalArgumentException("File exceeds maximum allowed size of 10 MB"); + } + + // Validate content type + String contentType = file.getContentType(); + if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) { + throw new IllegalArgumentException("Unsupported document type: " + contentType + + ". Allowed: PDF, JPEG, PNG"); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/file/FileService.java` around lines 97 - 108, Replace the use of IOException for validation failures in FileService.uploadFile with a validation-specific unchecked exception (create a new InvalidUploadException extends RuntimeException) so client errors map to 400; keep IOException for real I/O problems in the method signature, but throw new InvalidUploadException(...) instead of new IOException(...) for the size check and the content-type check (references: uploadFile method in FileService and the two throw sites that validate MAX_FILE_SIZE_BYTES and ALLOWED_CONTENT_TYPES); add the new exception class and import it, and ensure VisaViewController (or the web layer) can map InvalidUploadException to HTTP 400.
50-50: Hardcoded"test-bucket"sentinel for skipping initialization.Relying on a magic bucket-name string to detect the test profile is brittle — any production bucket accidentally named
test-bucketwould silently skip initialization, and vice versa. Prefer a Spring profile check (@Profile/@ConditionalOnProperty) or an explicit flag likeminio.initializeBucket=falseinapplication-test.properties.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/visacasemanagementsystem/file/FileService.java` at line 50, The FileService currently uses a hardcoded sentinel check (bucketName == null || bucketName.equals("test-bucket")) to skip bucket initialization; replace that brittle logic with a proper profile/property-driven guard: remove the "test-bucket" string check in the initialization path (the code referencing bucketName in FileService) and instead inject/consume a Spring boolean property (e.g., minio.initializeBucket) or annotate the bean with `@Profile/`@ConditionalOnProperty so initialization only runs when explicitly enabled; update the initialization method/constructor (initBucket or whatever method performs creation) to consult this property/annotation and fall back to checking only for null bucketName, ensuring production buckets named "test-bucket" are not treated specially.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/main/java/org/example/visacasemanagementsystem/file/FileService.java`:
- Around line 129-142: getPresignedDownloadUrl currently sets
responseContentDisposition to the full S3 key (which includes the internal UUID)
so users download filenames like "<UUID>_original"; instead, change
getPresignedDownloadUrl to look up the stored original/sanitized filename (from
object metadata or your DB) and pass only that user-friendly name to
GetObjectRequest.responseContentDisposition while keeping the UUID-prefixed S3
key for bucketName/key; update the method to fetch the original filename (via
your metadata lookup), set responseContentDisposition to "attachment;
filename=\"<originalFilename>\"" and continue using s3Presigner.presignGetObject
with the existing GetObjectPresignRequest.
- Around line 97-108: Replace the use of IOException for validation failures in
FileService.uploadFile with a validation-specific unchecked exception (create a
new InvalidUploadException extends RuntimeException) so client errors map to
400; keep IOException for real I/O problems in the method signature, but throw
new InvalidUploadException(...) instead of new IOException(...) for the size
check and the content-type check (references: uploadFile method in FileService
and the two throw sites that validate MAX_FILE_SIZE_BYTES and
ALLOWED_CONTENT_TYPES); add the new exception class and import it, and ensure
VisaViewController (or the web layer) can map InvalidUploadException to HTTP
400.
- Line 50: The FileService currently uses a hardcoded sentinel check (bucketName
== null || bucketName.equals("test-bucket")) to skip bucket initialization;
replace that brittle logic with a proper profile/property-driven guard: remove
the "test-bucket" string check in the initialization path (the code referencing
bucketName in FileService) and instead inject/consume a Spring boolean property
(e.g., minio.initializeBucket) or annotate the bean with
`@Profile/`@ConditionalOnProperty so initialization only runs when explicitly
enabled; update the initialization method/constructor (initBucket or whatever
method performs creation) to consult this property/annotation and fall back to
checking only for null bucketName, ensuring production buckets named
"test-bucket" are not treated specially.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 63a9979a-64eb-496e-9ad0-8ea5d32d9d85
📒 Files selected for processing (2)
src/main/java/org/example/visacasemanagementsystem/file/FileService.javasrc/main/resources/templates/visa/details.html
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/resources/templates/visa/details.html
Summary by CodeRabbit
New Features
Tests